-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local log = require 'mc.logging'
local utils = require 'mc.utils'
local singleton = require 'mc.singleton'
local cjson = require 'cjson'
local array_collection = require 'array.array_collection'
local volume_collection = require 'volume.volume_collection'
local drive_collection = require 'drive.drive_collection'
local common_def = require 'common_def'

local c_link_volume_array_drive_service = class()

function c_link_volume_array_drive_service:ctor()
end

local function check_drive_absent(pre_drive_list, cur_drive_list, volume_state)
    for _, drive_name in ipairs(pre_drive_list) do
        if not cur_drive_list[drive_name] then
            local drive = drive_collection:get_drive(drive_name)
            if drive.Presence == 0 then
                drive:generate_in_failed_array(true, 'OOB', volume_state)
            end
        end
    end
end

local function recalculatePdCount(arrayPdCount, raidLevel)
    if raidLevel == common_def.RAID_LEVEL_1 or raidLevel == common_def.RAID_LEVEL_10 then
        if arrayPdCount % 2 ~= 0 then
            return nil
        end
        return arrayPdCount / 2
    elseif raidLevel == common_def.RAID_LEVEL_5 or raidLevel == common_def.RAID_LEVEL_50 then
        if arrayPdCount < 3 then
            return nil
        end
        return arrayPdCount - 1
    elseif raidLevel == common_def.RAID_LEVEL_6 or raidLevel == common_def.RAID_LEVEL_60 then
        if arrayPdCount < 3 then
            return nil
        end
        return arrayPdCount - 2
    elseif raidLevel == common_def.RAID_LEVEL_1ADM or
           raidLevel == common_def.RAID_LEVEL_10ADM or
           raidLevel == common_def.RAID_LEVEL_1TRIPLE or
           raidLevel == common_def.RAID_LEVEL_10TRIPLE then
        if arrayPdCount % 3 ~= 0 then
            return nil
        end
        return arrayPdCount / 3
    else
        return arrayPdCount
    end
end

-- diskarray获取到的关联逻辑盘可能是不存在的，在给硬盘赋值关联逻辑盘时要先判断逻辑盘是否存在
function c_link_volume_array_drive_service:update_drive_ref_volumes(drive, array_obj)
    local real_volumes = {}
    local volume

    if log:getLevel() >= log.INFO then
        log:info('array.RefVolumes:%s array.RefDrives:%s.', cjson.encode(array_obj.RefVolumes),
            cjson.encode(array_obj.RefDrives))
    end
    for _, v in pairs(array_obj.RefVolumes) do
        volume = volume_collection.get_instance():get_ctrl_volume_by_id(drive.RefControllerId, v)
        if not volume then
            goto continue
        end
        log:info('volume.RefDriveList:%s.', cjson.encode(volume.RefDriveList))
        for _, drive_name in pairs(volume.RefDriveList) do
            if drive_name == drive.Name then
                table.insert(real_volumes, v)
                break
            end
        end
        ::continue::
    end
    if log:getLevel() >= log.INFO then
        log:info('Update drive:%s ref volume list:%s by array:%s.', drive.Id, cjson.encode(real_volumes), array_obj.Id)
    end
    drive.RefVolumeList = real_volumes
end

function c_link_volume_array_drive_service:update_volume_ref_drive_list()
    volume_collection.get_instance().on_update_drive_list:on(function(volume_obj)
        local array = nil
        local cur_drive_list, ref_drive_list = {}, {}
        for _, array_id in pairs(volume_obj.RefDiskArrayList) do
            array = array_collection.get_instance():get_ctrl_array_by_id(volume_obj.RefController, array_id)
            if array and array.Id < 0x8000 then
                for _, drive_name in pairs(array.RefDrives) do
                    table.insert(ref_drive_list, drive_name)
                    cur_drive_list[drive_name] = true
                end
            end
        end

        if not utils.table_compare(ref_drive_list, volume_obj.RefDriveList) then
            volume_obj.RefDriveList = ref_drive_list
        end

        if volume_obj.State == common_def.LD_STATE.OPTIMAL then
            if #volume_obj.RefDriveList > #volume_obj.optimal_drive_list then
                volume_obj.optimal_drive_list = utils.table_copy(volume_obj.RefDriveList)
            end
        elseif volume_obj.State == common_def.LD_STATE.OFFLINE or
            volume_obj.State == common_def.LD_STATE.DEGRADED or
            volume_obj.State == common_def.LD_STATE.PARTIALLY_DEGRADED or
            volume_obj.State == common_def.LD_STATE.FAILED then
            check_drive_absent(volume_obj.optimal_drive_list, cur_drive_list, volume_obj.State)
        end
    end)
end

function c_link_volume_array_drive_service:init()
    self:update_volume_ref_drive_list()

    volume_collection.get_instance().on_update_spare_drive_list:on(function(volume_obj, pd_ids)
        local spare_drive_list = {}
        for _, pd_id in pairs(pd_ids) do
            local drive_obj = drive_collection:get_drive_by_device_id(volume_obj.RefController, pd_id)
            if drive_obj then
                table.insert(spare_drive_list, drive_obj.Name)
            end
        end
        volume_obj.HotSpareDriveList = spare_drive_list
    end)

    volume_collection.get_instance().on_clear_drives_failed_array_alarm:on(function(volume_obj)
        for _, drive_name in ipairs(volume_obj.optimal_drive_list) do
            local drive = drive_collection:get_drive(drive_name)
            drive:generate_in_failed_array(false, 'OOB', common_def.LD_STATE.OFFLINE)
            drive:generate_in_failed_array(false, 'OOB', common_def.LD_STATE.DEGRADED)
        end
    end)

    volume_collection.get_instance().on_update_array_by_volume:on(function(obj)
        if not obj then
            return
        end

        for _, v in pairs(obj.RefDiskArrayList) do
            local array =
                array_collection.get_instance():get_ctrl_array_by_id(obj.RefControllerId, v)
            if not array then
                -- 对于PMC和1880卡来说，v>0x8000的不是Array，是Span编号
                if v < 0x8000 then
                    log:error('[Storage]Invalid RefArrayId %d, ctrl id is %s', v, obj.RefControllerId)
                end
            else
                array.RAIDType = tostring(obj.RAIDType)
                array.DriveNumPerSpan = obj.NumDrivePerSpan
                local arrayPdCount = recalculatePdCount(obj.NumDrivePerSpan, obj.RAIDType)
                local res = arrayPdCount and math.floor(array.TotalFreeSpaceMiB / arrayPdCount) or
                    common_def.INVALID_U16
                array.AverageDriveFreeSpaceMiB = res
            end
        end
    end)

    drive_collection.get_instance().on_update_volume_list:on(function(drive_obj)
        -- 检查DiskArrayId是否还存在
        local array = array_collection.get_instance():get_ctrl_array_by_id(drive_obj.RefControllerId,
            drive_obj.RefDiskArrayId)
        -- 如果DiskArrayId不存在，则删除硬盘内的关联RefArray信息
        if not array then
            drive_obj.RefDiskArrayId = common_def.INVALID_U16
        else
            -- 如果DiskArrayId存在，但对应的DiskArray的RefDrives里没有这个硬盘，则删除硬盘内的关联RefArray信息
            local count = 0
            for _, drive_name in pairs(array.RefDrives) do
                if drive_name == drive_obj.Name then
                    count = count + 1
                end
            end
            if count == 0 then
                drive_obj.RefDiskArrayId = common_def.INVALID_U16
            end
        end

        -- 检查逻辑盘是否存在,如果有不存在的就删除,然后直接返回
        local volume = nil
        local prop_value = drive_obj.RefVolumeList
        local prop_changed = false
        for pos, volume_id in pairs(prop_value) do
            volume = volume_collection.get_instance():get_ctrl_volume_by_id(drive_obj.RefControllerId, volume_id)
            if not volume then
                table.remove(prop_value, pos)
                drive_obj.RefVolumeList = prop_value -- 需要用修改后的数据显式地设置属性值，修改才会生效
                return
            else
                -- 如果逻辑盘存在，但对应的Volume的RefDriveList里没有这个硬盘，则删除硬盘内的关联RefVolumeList信息
                local count = 0
                for _, drive_name in pairs(volume.RefDriveList) do
                    if drive_name == drive_obj.Name then
                        count = count + 1
                    end
                end
                if count == 0 then
                    table.remove(prop_value, pos)
                    prop_changed = true
                end
            end
        end
        if prop_changed then
            drive_obj.RefVolumeList = prop_value -- 需要用修改后的数据显式地设置属性值，修改才会生效
        end
    end)

    array_collection.get_instance().on_update_drive_list:on(function(array_obj)
        local pd_idx = 1
        local drive = nil
        local ref_drives = {}
        for _, slot in pairs(array_obj.RefPDSlots) do
            local enclosure = array_obj.RefPDEnclosures[pd_idx]
            drive = drive_collection.get_instance():get_drive_by_physic_id(array_obj.RefControllerId, slot, enclosure)
            if drive then
                drive.RefDiskArrayId = array_obj.Id >= 0x8000 and drive.RefDiskArrayId or array_obj.Id
                self:update_drive_ref_volumes(drive, array_obj)
                table.insert(ref_drives, drive.Name)
            end
            pd_idx = pd_idx + 1
        end
        if not utils.table_compare(ref_drives, array_obj.RefDrives) then
            array_obj.RefDrives = ref_drives
        end
    end)
end

return singleton(c_link_volume_array_drive_service)